package sk.upjs.ariadne.editor;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D.Double;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

public class MapPanel extends JPanel {

	// ----------------------------------------------------------------------
	// Layer management - instance variables
	// ----------------------------------------------------------------------

	/**
	 * The model for a list of map layers.
	 */
	private class MapLayersModel extends DefaultComboBoxModel<MapLayer> {
		@Override
		public void addElement(MapLayer aLayer) {
			if (aLayer != null) {
				aLayer.setParent(MapPanel.this);
				super.addElement(aLayer);
			}
			repaint();
		}

		@Override
		public void insertElementAt(MapLayer aLayer, int index) {
			if (aLayer != null) {
				MapPanel oldPanel = aLayer.getParent();
				aLayer.setParent(MapPanel.this);
				try {
					super.insertElementAt(aLayer, index);
				} catch (Exception e) {
					aLayer.setParent(oldPanel);
				}

				repaint();
			}
		}

		@Override
		public void removeElementAt(int index) {
			MapLayer layer = getElementAt(index);
			if (layer != null)
				layer.setParent(null);

			super.removeElementAt(index);
			repaint();
		}

		@Override
		public void removeAllElements() {
			for (int i = 0; i < getSize(); i++)
				getElementAt(i).setParent(null);

			super.removeAllElements();
			repaint();
		}

		@Override
		public void removeElement(Object aLayer) {
			if (getIndexOf(aLayer) >= 0)
				((MapLayer) aLayer).setParent(null);

			super.removeElement(aLayer);
			repaint();
		}

		@Override
		public void setSelectedItem(Object anObject) {
			if (anObject == null) {
				if (editedLayer != null) {
					editedLayer.stopLayerEditing();
					editedLayer = null;
				}
			} else {
				if (getIndexOf(anObject) < 0)
					return;

				if (anObject == editedLayer)
					return;

				if (editedLayer != null)
					editedLayer.stopLayerEditing();

				editedLayer = (MapLayer) anObject;
				editedLayer.startLayerEditing();
			}

			super.setSelectedItem(anObject);
			repaint();
		}
	}

	/**
	 * List of all map layers that are displayed in this panel.
	 */
	private MapLayersModel mapLayers;

	/**
	 * A map layer that is currently edited.
	 */
	private MapLayer editedLayer;

	// ----------------------------------------------------------------------
	// View management - instance variable
	// ----------------------------------------------------------------------

	/**
	 * Width of the map.
	 */
	private double mapWidth;

	/**
	 * Height of the map.
	 */
	private double mapHeight;

	/**
	 * Zoom used to display content of layers.
	 */
	private double zoom = 1;

	// ----------------------------------------------------------------------
	// GUI components
	// ----------------------------------------------------------------------

	/**
	 * Core class for displaying map layers.
	 */
	private class DrawingPane extends JPanel {

		@Override
		protected void paintComponent(Graphics g) {
			super.paintComponent(g);
			Graphics2D g2 = (Graphics2D) g;

			// transform clip bounds to map coordinates
			Rectangle clip = g2.getClipBounds();
			if (clip == null)
				clip = new Rectangle(0, 0, getWidth(), getHeight());

			// restrict clip bounds to map size
			clip = clip.intersection(new Rectangle((int) (mapWidth * zoom),
					(int) (mapHeight * zoom)));
			g2.setClip(clip);

			g2.setPaint(Color.white);
			g2.fill(clip);

			Rectangle2D mapClip = new Rectangle2D.Double(clip.getX() / zoom,
					clip.getY() / zoom, clip.getWidth() / zoom,
					clip.getHeight() / zoom);

			// repaint map layers
			Graphics2D layerGraphics;
			for (int i = 0; i < mapLayers.getSize(); i++) {
				MapLayer mapLayer = mapLayers.getElementAt(i);
				layerGraphics = (Graphics2D) g2.create();
				try {
					mapLayer.paint(layerGraphics, mapClip);
				} finally {
					layerGraphics.dispose();
				}
			}

			// paint edit overlay of the edited layer
			if (editedLayer != null) {
				layerGraphics = (Graphics2D) g2.create();
				try {
					editedLayer.paintEditOverlay(layerGraphics, mapClip);
				} finally {
					layerGraphics.dispose();
				}
			}
		}
	}

	/**
	 * ScrollPane for displaying the drawing pane.
	 */
	private JScrollPane scrollPane;

	/**
	 * A panel for displaying all map layers.
	 */
	private DrawingPane drawingPane;

	// ----------------------------------------------------------------------
	// ----------------------------------------------------------------------

	public MapPanel() {
		// create model for map layers
		mapLayers = new MapLayersModel();
		createGUI();
		setMapSize(600, 600);
	}

	// ----------------------------------------------------------------------
	// Map layers
	// ----------------------------------------------------------------------

	/**
	 * Returns the model for accessing and ordering map layers.
	 * 
	 * @return the model of map layers.
	 */
	public DefaultComboBoxModel<MapLayer> getMapLayers() {
		return mapLayers;
	}

	// ----------------------------------------------------------------------
	// GUI and painting
	// ----------------------------------------------------------------------

	/**
	 * Creates all GUI subelements of this map panel.
	 */
	private void createGUI() {
		setLayout(new BorderLayout());
		drawingPane = new DrawingPane();

		// install listeners
		drawingPane.addMouseListener(new MouseListener() {

			@Override
			public void mouseReleased(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseReleased(
							panelToMapCoordinates(e.getPoint()), e);
			}

			@Override
			public void mousePressed(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMousePressed(
							panelToMapCoordinates(e.getPoint()), e);
			}

			@Override
			public void mouseExited(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseExited(
							panelToMapCoordinates(e.getPoint()), e);
			}

			@Override
			public void mouseEntered(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseEntered(
							panelToMapCoordinates(e.getPoint()), e);
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseClicked(
							panelToMapCoordinates(e.getPoint()), e);
			}
		});

		drawingPane.addMouseMotionListener(new MouseMotionListener() {
			@Override
			public void mouseMoved(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseMoved(
							panelToMapCoordinates(e.getPoint()), e);

			}

			@Override
			public void mouseDragged(MouseEvent e) {
				if (editedLayer != null)
					editedLayer.onMouseDragged(
							panelToMapCoordinates(e.getPoint()), e);
			}
		});

		// add components
		scrollPane = new JScrollPane(drawingPane);
		add(scrollPane, BorderLayout.CENTER);
	}

	/**
	 * Updates properties of the drawing pane in order to reflect current
	 * settings of the map.
	 */
	private void updateDrawingPane() {
		drawingPane.setPreferredSize(new Dimension((int) (mapWidth * zoom),
				(int) (mapHeight * zoom)));
		drawingPane.revalidate();
		drawingPane.repaint();
	}

	/**
	 * Receives a request from a map layer to repaint the panel content.
	 * 
	 * @param mapLayer
	 *            a map layer that requested repaint
	 */
	void forceRepaintByLayer(MapLayer mapLayer) {
		repaint();
	}

	/**
	 * Transforms coordinates in the drawing panel to map coordinates.
	 * 
	 * @param panelCoordinates
	 *            coordinates in the drawing panel
	 * @return transformed map coordinates
	 */
	private Point2D panelToMapCoordinates(Point panelCoordinates) {
		return new Point2D.Double(panelCoordinates.getX() / zoom,
				panelCoordinates.getY() / zoom);
	}

	/**
	 * Sets the dimensions of the map.
	 * 
	 * @param newMapWidth
	 *            the desired width of the map
	 * @param newMapHeight
	 *            the desired height of the map
	 */
	public void setMapSize(double newMapWidth, double newMapHeight) {
		if (newMapWidth < 1)
			newMapWidth = 1;

		if (newMapHeight < 1)
			newMapHeight = 1;

		if ((newMapWidth == mapWidth) && (newMapHeight == mapHeight))
			return;

		mapWidth = newMapWidth;
		mapHeight = newMapHeight;
		updateDrawingPane();
	}

	/**
	 * Returns the map width.
	 * 
	 * @return the width of the map.
	 */
	public double getMapWidth() {
		return mapWidth;
	}

	/**
	 * Sets the width of the map.
	 * 
	 * @param mapWidth
	 *            the desired width of the map.
	 */
	public void setMapWidth(int mapWidth) {
		setMapSize(mapWidth, mapHeight);
	}

	/**
	 * Returns the map height.
	 * 
	 * @return the height of the map.
	 */
	public double getMapHeight() {
		return mapHeight;
	}

	/**
	 * Sets the height of the map.
	 * 
	 * @param mapHeight
	 *            the desired height of the map.
	 */
	public void setMapHeight(int mapHeight) {
		setMapSize(mapWidth, mapHeight);
	}

	/**
	 * Returns the current zoom of this map panel.
	 * 
	 * @return the zoom.
	 */
	public double getZoom() {
		return zoom;
	}

	/**
	 * Sets the desired zoom of this map panel.
	 * 
	 * @param zoom
	 *            the desired zoom.
	 */
	public void setZoom(double zoom) {
		if (zoom <= 0)
			return;

		if (this.zoom == zoom)
			return;

		this.zoom = zoom;

		// notify all map layers about change of the zoom
		for (int i = 0; i < mapLayers.getSize(); i++)
			mapLayers.getElementAt(i).onZoomChanged(zoom);

		updateDrawingPane();
	}

	/**
	 * Sets the map dimensions to fit content of all map layers.
	 */
	public void autoSize() {
		Rectangle2D minimalArea = null;
		Rectangle2D layerRect;
		for (int i = 0; i < mapLayers.getSize(); i++) {
			layerRect = mapLayers.getElementAt(i).getBoundingRect();
			if (minimalArea == null) {
				minimalArea = layerRect;
			} else {
				if (layerRect != null)
					minimalArea = minimalArea.createUnion(layerRect);
			}
		}

		if (minimalArea != null)
			setMapSize(minimalArea.getMaxX(), minimalArea.getMaxY());
	}
}
